package com.foreverwin.mes.integration.odata;

import com.google.common.collect.Lists;
import okhttp3.*;
import okio.Buffer;
import okio.BufferedSource;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.olingo.odata2.api.ODataCallback;
import org.apache.olingo.odata2.api.commons.HttpContentType;
import org.apache.olingo.odata2.api.commons.HttpHeaders;
import org.apache.olingo.odata2.api.commons.HttpStatusCodes;
import org.apache.olingo.odata2.api.edm.Edm;
import org.apache.olingo.odata2.api.edm.EdmEntityContainer;
import org.apache.olingo.odata2.api.edm.EdmEntitySet;
import org.apache.olingo.odata2.api.edm.EdmException;
import org.apache.olingo.odata2.api.ep.EntityProvider;
import org.apache.olingo.odata2.api.ep.EntityProviderException;
import org.apache.olingo.odata2.api.ep.EntityProviderReadProperties;
import org.apache.olingo.odata2.api.ep.EntityProviderWriteProperties;
import org.apache.olingo.odata2.api.ep.callback.OnWriteFeedContent;
import org.apache.olingo.odata2.api.ep.callback.WriteFeedCallbackContext;
import org.apache.olingo.odata2.api.ep.callback.WriteFeedCallbackResult;
import org.apache.olingo.odata2.api.ep.entry.ODataEntry;
import org.apache.olingo.odata2.api.ep.feed.ODataFeed;
import org.apache.olingo.odata2.api.exception.ODataApplicationException;
import org.apache.olingo.odata2.api.exception.ODataRuntimeApplicationException;
import org.apache.olingo.odata2.api.processor.ODataErrorContext;
import org.apache.olingo.odata2.api.processor.ODataResponse;
import org.apache.olingo.odata2.api.uri.ExpandSelectTreeNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.text.MessageFormat;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;

/**
 * @author Ervin Chen
 * @date 2020/1/13 13:30
 */
public class ODataConsumer {

    public static final String METADATA = "$metadata";
    public static final String SEPARATOR = "/";
    public static final String CSRF_TOKEN_HEADER = "X-CSRF-Token";
    public static final String CSRF_TOKEN_FETCH = "Fetch";

    private static final Logger logger = LoggerFactory.getLogger(ODataConsumer.class);

    private OkHttpClient m_okHttpClient = null;
    private Edm m_edm = null;
    private String m_csrfToken = null;
    private String m_cookie = null;
    private String m_etag = null;
    private String serviceUrl = null;
    private String username = null;
    private String password = null;

    private ODataConsumer(String serviceUrl, String username, String password) {
        this.serviceUrl = serviceUrl;
        this.username = username;
        this.password = password;
    }

    public OkHttpClient getOkHttpClient() {
        if (this.m_okHttpClient == null) {
            this.m_okHttpClient = new OkHttpClient.Builder().build();
        }
        return this.m_okHttpClient;
    }

    private Edm readEdm()
            throws EntityProviderException, IllegalStateException, IOException {
        // This is used for both setting the Edm and CSRF Token :)
        if (m_edm != null) {
            return m_edm;
        } else {
            excuteReadEdm();
        }
        return m_edm;
    }

    public ODataFeed readFeed(String contentType,
                              String entitySetName,
                              SystemQueryOptions options)
            throws IllegalStateException, IOException, EntityProviderException, EdmException {
        String absoluteUri = createUri(this.serviceUrl, entitySetName, null, options);
        Response okResponse = executeGet(absoluteUri, contentType);
        logOdataOperate("readFeed", okResponse);
        InputStream content = okResponse.body().byteStream();
        EdmEntitySet entitySet = getEdmEntitySet(entitySetName);
        return EntityProvider.readFeed(contentType,
                entitySet,
                content,
                EntityProviderReadProperties.init().build());
    }

    public ODataEntry readEntry(String contentType,
                                String entitySetName,
                                String keyValue,
                                SystemQueryOptions options)
            throws IllegalStateException, IOException, EdmException, EntityProviderException, ODataApplicationException {
        String absoluteUri = createUri(this.serviceUrl, entitySetName, keyValue, options);
        Response okResponse = executeGet(absoluteUri, contentType);
        logOdataOperate("readEntry", okResponse);
        InputStream content = okResponse.body().byteStream();
        EdmEntitySet entitySet = getEdmEntitySet(entitySetName);
        HttpStatusCodes statusCode = HttpStatusCodes.fromStatusCode(okResponse.code());
        if (!(statusCode == HttpStatusCodes.OK)) {
            throwError(content, contentType);
        }
        return EntityProvider.readEntry(contentType,
                entitySet,
                content,
                EntityProviderReadProperties.init().build());
    }

    public ODataEntry createEntry(String contentType,
                                  String entitySetName,
                                  Map<String, Object> data,
                                  Map<String, Map<String, Object>> additionalLinks,
                                  Map<String, ODataCallback> callBacks) throws Exception {
        Response response = writeEntity(contentType, entitySetName, null, data, additionalLinks, callBacks);
        logOdataOperate("createEntry", response);
        HttpStatusCodes statusCode = HttpStatusCodes.fromStatusCode(response.code());
        InputStream content = response.body().byteStream();
        if (!(statusCode == HttpStatusCodes.CREATED)) {
            throwError(content, contentType);
        }
        // get the content as InputStream and de-serialize it into an ODataEntry object
        ODataEntry entry = null;
        EdmEntitySet entitySet = getEdmEntitySet(entitySetName);
        entry = EntityProvider.readEntry(
                contentType,
                entitySet,
                content,
                EntityProviderReadProperties.init().build());
        return entry;
    }

    public void updateEntity(String contentType,
                             String entitySetName,
                             String keyValue,
                             Map<String, Object> data,
                             Map<String, Map<String, Object>> additionalLinks,
                             Map<String, ODataCallback> callBacks)
            throws EdmException, URISyntaxException, EntityProviderException, IOException, ODataApplicationException {
        Response response = writeEntity(contentType, entitySetName, keyValue, data, additionalLinks, callBacks);
        HttpStatusCodes statusCode = HttpStatusCodes.fromStatusCode(response.code());
        InputStream content = response.body().byteStream();
        if (!(statusCode == HttpStatusCodes.NO_CONTENT)) {
            throwError(content, contentType);
        }
        logOdataOperate("updateEntity", response);
    }

    public void deleteEntry(String contentType,
                            String entitySetName,
                            String keyValue,
                            SystemQueryOptions options)
            throws IllegalStateException, IOException, EntityProviderException, ODataApplicationException {
        String absoluteUri = createUri(this.serviceUrl, entitySetName, keyValue, options);
        Response okResponse = executeDelete(absoluteUri, contentType);
        logOdataOperate("deleteEntry", okResponse);
        InputStream content = okResponse.body().byteStream();
        HttpStatusCodes statusCode = HttpStatusCodes.fromStatusCode(okResponse.code());
        if (!(statusCode == HttpStatusCodes.NO_CONTENT)) {
            throwError(content, contentType);
        }
    }

    public Response writeEntity(String contentType,
                                String entitySetName,
                                String keyValue,
                                Map<String, Object> data,
                                Map<String, Map<String, Object>> additionalLinks,
                                Map<String, ODataCallback> callBacks)
            throws EdmException, IOException, EntityProviderException, URISyntaxException {

        EdmEntitySet entitySet = getEdmEntitySet(entitySetName);
        URI rootUri = new URI(entitySetName);
        ExpandSelectTreeNode node = ExpandSelectTreeNode.entitySet(entitySet).expandedLinks(Lists.newArrayList(additionalLinks.keySet())).build();
        EntityProviderWriteProperties properties = EntityProviderWriteProperties
                .serviceRoot(rootUri)
                .omitJsonWrapper(true)
                .additionalLinks(additionalLinks)
                .expandSelectTree(node)
                .callbacks(callBacks)
                .build();
        // serialize data into ODataResponse object
        ODataResponse response = EntityProvider.writeEntry(contentType, entitySet, data, properties);
        // get (http) entity which is for default Olingo implementation an InputStream
        Object entity = response.getEntity();
        Response okResponse = null;
        if (entity instanceof InputStream) {
            String absoluteUri = createUri(serviceUrl, entitySetName, keyValue, null);
            String content = streamToString((InputStream) entity);
            okResponse = executePost(absoluteUri, contentType, content);
        }

        return okResponse;
    }

    public EdmEntitySet getEdmEntitySet(String entitySetName)
            throws EdmException, IOException, EntityProviderException {
        Edm edm = readEdm();
        EdmEntityContainer entityContainer = edm.getDefaultEntityContainer();
        EdmEntitySet entitySet = entityContainer.getEntitySet(entitySetName);
        return entitySet;
    }

    private Response excuteReadEdm() throws IOException, EntityProviderException {
        String serviceUrl = createMetaUri();
        Request request = new Request.Builder()
                .header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader())
                .header(CSRF_TOKEN_HEADER, CSRF_TOKEN_FETCH)
                .url(serviceUrl)
                .build();
        Response response = getOkHttpClient().newCall(request).execute();
        m_cookie = response.header(HttpHeaders.SET_COOKIE);
        m_csrfToken = response.header(CSRF_TOKEN_HEADER);
        m_etag = response.header(HttpHeaders.ETAG);
        m_etag = m_etag == null ? "" : m_etag;
        m_edm = EntityProvider.readMetadata(response.body().byteStream(), false);
        logger.info("Metadata url => {}", serviceUrl);
        logger.info("CSRF token => {}", m_csrfToken);
        logger.info("CSRF token => {}", m_csrfToken);
        logger.info("Cookie => {}", m_cookie);
        logger.info("etag => {}", m_etag);
        return response;
    }

    private Response executeGet(String absoluteUrl, String contentType)
            throws IllegalStateException, IOException {
        Request request = new Request.Builder()
                .header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader())
                .header(HttpHeaders.ACCEPT, contentType)
                .url(absoluteUrl)
                .get()
                .build();
        Response okResponse = getOkHttpClient().newCall(request).execute();
        return okResponse;
    }

    private Response executePost(String absoluteUrl, String contentType, String body)
            throws IllegalStateException, IOException {
        Request request = new Request.Builder()
                .header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader())
                .header(HttpHeaders.COOKIE, m_cookie)
                .header(CSRF_TOKEN_HEADER, m_csrfToken)
                .header(HttpHeaders.IF_MATCH, m_etag)
                .header(HttpHeaders.AUTHORIZATION, HttpContentType.APPLICATION_ATOM_XML)
                .url(absoluteUrl)
                .post(RequestBody.create(MediaType.parse(contentType), body))
                .build();
        Response okResponse = getOkHttpClient().newCall(request).execute();
        return okResponse;
    }

    private Response executeDelete(String absoluteUrl, String contentType)
            throws IllegalStateException, IOException {
        Request request = new Request.Builder()
                .header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader())
                .header(HttpHeaders.ACCEPT, contentType)
                .url(absoluteUrl)
                .delete()
                .build();
        Response okResponse = getOkHttpClient().newCall(request).execute();
        return okResponse;
    }

    public void logOdataOperate(String operate, Response response) throws IOException {
        Request request = response.request();
        HttpUrl url = request.url();
        Headers headers = request.headers();
        int code = response.code();
        ResponseBody responseBody = response.body();
        BufferedSource source = responseBody.source();
        source.request(Long.MAX_VALUE);
        Buffer buffer = source.buffer();
        String message = "";
        if (responseBody.contentLength() != 0) {
            message = buffer.clone().readString(Charset.forName("UTF-8"));
        }
        String s = MessageFormat.format("method: {0}\r\nurl: {1}\r\nheaders: {2}\rresponse code: {3}\r\nresponse message: {4}", operate, url, headers, code, message);
        System.out.println(s);
        logger.info("method: {}\r\nurl: {}\r\nheaders: {}\r\nresponse code: {}\rresponse message: {}", operate, url, headers, code, message);
    }

    private void throwError(InputStream content, String contentType) throws EntityProviderException, ODataApplicationException {
        ODataErrorContext errorContext = EntityProvider.readErrorDocument(content, HttpContentType.APPLICATION_ATOM_XML);
        throw new ODataApplicationException(errorContext.getMessage(), Locale.getDefault(), errorContext.getHttpStatus(), errorContext.getErrorCode());
    }

    /**
     * Note: This example uses Basic Authentication
     * Preferred option is to use OAuth SAML bearer flow.
     *
     * @return
     */
    private String getAuthorizationHeader() {
        String temp = new StringBuilder(this.username).append(":")
                .append(this.password).toString();
        String result = "Basic " + new String(Base64.encodeBase64(temp.getBytes()));
        logger.info("AuthorizationHeader " + result);
        return result;
    }

    private String streamToString(InputStream stream) throws IOException {
        StringWriter writer = new StringWriter();
        IOUtils.copy(stream, writer);
        return writer.toString();
    }

    private String createMetaUri() {
        StringBuilder absoluteUri = new StringBuilder(this.serviceUrl)
                .append(SEPARATOR).append(METADATA);
        return absoluteUri.toString();
    }

    private String createUri(String serviceUri,
                             String entitySetName,
                             String id,
                             SystemQueryOptions options) {
        final StringBuilder absoluteUri = new StringBuilder(serviceUri).append(SEPARATOR).append(entitySetName);
        if (id != null) {
            absoluteUri.append("(").append(id).append(")");
        }
        if (options != null) {
            if (options.getQueryCondition() != null) {
                absoluteUri.append(StringUtils.replace(options.getQueryCondition(), " ", "%20"));
            }
        }
        return absoluteUri.toString();
    }

    public static final class OnWriteFeedCallBack implements OnWriteFeedContent {
        private List<Map<String, Object>> feedData;

        public OnWriteFeedCallBack(List<Map<String, Object>> feedData) {
            this.feedData = feedData;
        }

        @Override
        public WriteFeedCallbackResult retrieveFeedResult(final WriteFeedCallbackContext writeFeedCallbackContext) {
            EntityProviderWriteProperties currentWriteProperties = writeFeedCallbackContext.getCurrentWriteProperties();
            WriteFeedCallbackResult result = new WriteFeedCallbackResult();
            result.setInlineProperties(currentWriteProperties);
            result.setFeedData(feedData);
            return result;
        }
    }

    public static final class SystemQueryOptions {
        private String queryCondition;

        public String getQueryCondition() {
            return queryCondition;
        }

        public void setQueryCondition(String queryCondition) {
            this.queryCondition = queryCondition;
        }
    }

    public static final class Builder {
        private String serviceUrl = null;
        private String username = null;
        private String password = null;

        public Builder serviceUrl(String serviceUrl) {
            this.serviceUrl = serviceUrl;
            return this;
        }

        public Builder username(String username) {
            this.username = username;
            return this;
        }

        public Builder password(String password) {
            this.password = password;
            return this;
        }

        public ODataConsumer build() {
            return new ODataConsumer(this.serviceUrl, this.username, this.password);
        }
    }
}
